#Requires -Modules Pester

if (-not (test-path Variable:\pester)) {
    throw "script must be run with Invoke-Pester.ps1"
}

$here = $PSScriptRoot
$scriptDir = join-path (Split-Path -Parent $PSScriptRoot) DevBoard


# import test support functions & mocks
. $here\testSupportFunctions.ps1

# import Script Under Test (SUT)
. $scriptDir\DevboardImage.ps1

Describe "Creating New WIM" {

    Context "When called with valid parameters & a destination WIM" {
        setupTestEnvironment -FakeImageCount 1
        $p = getDefaultParameters    
        
        It "doesn't throw an exception" {
            {New-DevboardImage @p} | Should Not Throw
        }
    
        It "creates a target image with the right set of drivers" {
            Assert-VerifiableMocks
            "TestDrive:\output\testimage.wim" | Should Exist
        }
    
        It "exports the source to the destination" {
            Assert-MockCalled -commandName Export-WindowsImage `
                              -parameterFilter {$SourceImagePath -eq $script:inputWim -and 
                                                $DestinationImagePath -eq $script:outputWim -and 
                                                $SourceIndex -eq 1} -Exactly 1
        }
    
        It "Adds 3 drivers to the image" {
            (Select-String -Path $script:outputWim -Pattern "ADD_DRIVER.*").Matches.Count | Should Be 3
        }
    
        It "Uses a dism.log file in the same directory as the destination WIM, rather than the global DISM log" {
            # NOTE: in other contexts, this may not call all of the mocks. If you copy this It, adjust the 
            #       list accordingly.
            $commands = @("Export-WindowsImage", "Mount-WindowsImage", "Dismount-WindowsImage", "Add-WindowsDriver")
            $expectedLogPath = join-path (split-path $script:outputWim -parent) "dism.log"
    
            foreach ($c in $commands) {
                Assert-MockCalled -commandName $c `
                                  -parameterFilter {$LogPath -eq $expectedLogPath}
            }
        }
    
        It "Dismounts the WIM and saves the changes" {
            Get-Content -Path $script:outputWim -Tail 1 | should be "SAVE 1"
        }
    }

    Context "When source is a multi-image WIM" {
        
        setupTestEnvironment -FakeImageCount 2
        $p = getDefaultParameters

        It "works fine on the N'th index" {
            $p2 = $p.Clone()
            {New-DevboardImage -Index 2 @p2} | Should Not Throw
        }

        It "imports the N'th image into the destination" {
            (Get-Content -Path $script:outputWim)[0] | Should Match "EXPORT 1 .* 2$"
        }

        It "Requires the index parameter when there are multiple images in the WIM" {
            [hashtable] $p2 = $p.Clone()
            $p2.Remove("Index")
            {New-DevboardImage @p2} | Should Throw "Must specify -Index when -SourcePath is a multi-image WIM"
        }
    }
    
    Context "When adding a driver fails" {
    
        setupTestEnvironment -FakeImageCount 1
        $p = getDefaultParameters    

        
        Mock Add-WindowsDriver `
            -parameterFilter {$Driver -like "*test3.inf"} `
            -mockWith {throw "failing driver entry"}
    
        Mock Remove-Item {} -verifiable -ParameterFilter {$Path -eq $script:outputWim} 
    
        It "throws an exception" {
            {New-DevboardImage @p} | should throw "failing driver entry"
        }
        
        It "cleans up if it fails in the middle of applying files" {
            Get-Content -Path $script:outputWim -Tail 1 | should be "DISCARD 1"
            Assert-MockCalled -commandName Remove-Item `
                              -parameterFilter {$Path -eq $script:outputWim}
        }
    }

}

Describe "Execute in place" {

    Context "When called to execute in place" {
        
        setupTestEnvironment -FakeImageCount 2
        $p = getDefaultParameters
        $p.Remove("Destination")

        It "does not throw an exception" {
            {New-DevboardImage -Index 2 @p} | should not throw
        }
    
        It "makes its the modifications to the input WIM" {
            (Select-String -Path $script:inputWim -Pattern "ADD_DRIVER 2 .*").Matches.Count | Should Be 3
            Get-Content -Path $script:inputWim -Tail 1 | Should Be "SAVE 2"
        }
    
        It "Uses a dism.log file in the current working directory, rather than the global DISM log" {
            # NOTE: in other contexts, this may not call all of the mocks. If you copy this It, adjust the 
            #       list accordingly.
            $commands = @("Mount-WindowsImage", "Dismount-WindowsImage", "Add-WindowsDriver")
            $expectedLogPath = ".\dism.log"
    
            foreach ($c in $commands) {
                Assert-MockCalled -commandName $c `
                                  -parameterFilter {$LogPath -eq $expectedLogPath}
            }
        }

        # Test mucks with $script:inputWim's content, which isn't cleared when we pop context
        # Clear it manually
        # createFakeWim -ImagePath $script:InputWim -ImageCount 2 -Force > $null
    }

    Context "When source is a multi-image WIM" {
        setupTestEnvironment -FakeImageCount 2
        $p = getDefaultParameters
        $p.Remove("Destination")

        It "works fine on the N'th index" {
            {New-DevboardImage -Index 2 @p} | Should Not Throw
        }

        It "mounts the Nth image" {
            Assert-MockCalled `
                -commandName Mount-WindowsImage `
                -parameterFilter {$ImagePath -eq $script:inputWim -and $Index -eq 2} `
                -Exactly 1
        }
    }
    
    Context "When there's a failure while executing in place" {
        setupTestEnvironment -FakeImageCount 2
        $p = getDefaultParameters
        $p.Remove("Destination")
    
        Mock Add-WindowsDriver `
            -parameterFilter {$Driver -like "*test3.inf"} `
            -mockWith {throw "failing driver entry"}
    
        Mock Remove-Item {} -verifiable -ParameterFilter {
            ($Path -eq $script:outputWim) -or ($Path -eq $script:inputWim)
        } 
    
        It "does throw an exception" {
            {New-DevboardImage -Index 2 @p} | should throw "failing driver entry"
        }
    
        It "makes all the modifications to the input WIM" {
            (Select-String -Path $script:inputWim -Pattern "ADD_DRIVER 2 .*").Matches.Count | Should Be 2
        }
    
        It "discards any changes that it made to the WIM" {
            Get-Content -Path $script:inputWim -Tail 1 | Should Be "DISCARD 2"
        }
    
        It "does not delete the input WIM" {
            Assert-MockCalled -commandName Remove-Item `
                              -parameterFilter {$Path -eq $script:inputWim} `
                              -exactly 0
        }
    
        # Test mucks with $script:inputWim's content, which isn't cleared when we pop context
        # Clear it manually
        createFakeWim -ImagePath $script:InputWim -ImageCount 2 -Force > $null
    }

}

Describe "Handling Mount Directory" {

    setupTestEnvironment    

    $p = getDefaultParameters
    $p.Remove("MountDir")

    Context "When the mount directory isn't a directory" {
        $badMountDir = New-Item -ItemType file -Path TestDrive:\bad_mount
    
        It "throws an exception" {
            {New-DevboardImage @p -MountDir "$badMountDir"} | 
                should throw "An item with the specified name $badMountDir already exists"
        }
    }
    
    Context "When the mount directory doesn't exist" {
        $root = Convert-Path -LiteralPath TestDrive:\
    
        It "does not throw an exception" {
            {New-DevboardImage @p -MountDir "$root\new_mount" | Out-Null} | should not throw
        }
    
        It "creates the mount directory" {
            Test-Path -Path "$root\new_mount" -PathType Container | should be $true
        }
    
        remove-item -Path "$root\new_mount" -Recurse -Force
    }

    Context "When mount dir is not specified" {

        # temporary script-level variable to keep track of what directory the script
        # uses for the mount directory.
        $script:foundMountDir = $null

        # Mock new-item to keep track of the mount directory the script
        # creates.
        Mock New-Item -parameterFilter {
            ($Path -like "$env:TEMP\scmount-*") -and 
            ((split-path -Leaf $Path) -like "scmount-*") -and 
            ($ItemType -eq "Directory")
        } -mockWith {
            $script:foundMountDir = $Path
            Microsoft.PowerShell.Management\New-Item -Path $Path -ItemType $ItemType
        } -verifiable

        Mock Remove-Item -parameterFilter {
            $Path -eq $script:foundMountDir -and
            $Force -ne $null -and 
            $Force.IsPresent -eq $true -and 
            $Recurse -ne $null -and
            $Recurse.IsPresent -eq $true
        } -mockWith {
            Microsoft.PowerShell.Management\Remove-Item -Path $Path -Force -Recurse 
        } -verifiable

        It "does not throw an exception" {
            {New-DevboardImage @p} | should not throw
        }

        It "created a temporary mount directory" {
            Assert-VerifiableMocks
            $script:foundMountDir | Should Not Be $null
        }

        It "removed the temporary directory when finished" {
            $script:foundMountDir | Should Not Exist
        }

        # remove polution of script scope
        Remove-Variable -name foundMountDir -scope script
    }
}

Describe "Copying Tools" {

    setupTestEnvironment

    $p = getDefaultParameters

    Context "getWdkRoot always finds the WDK" {
        
        It "returns the supplied path, if the path is non-null" {
            $ENV:WDKContentRoot = "$script:inputDir\WDK"
    
            getWdkRoot "$script:inputDir\WDK" | Should Be "$script:inputDir\WDK"
    
            Remove-Item ENV:\WDKContentRoot
        }
    
        It "uses the WDKContentRoot environment variable if path was null" {
            $ENV:WDKContentRoot = "$script:inputDir\WDK"
    
            getWdkRoot | Should Be "$script:inputDir\WDK"
    
            Remove-Item ENV:\WDKContentRoot
        }
    
        It "Uses the highest version WDK listed in the registry otherwise" {
    
            # make a fake registry key and then have the mocks redirect to that.
            $keyName = 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots'
            $fakeKeyName = "HKCU:\temp"
    
            $fk = New-Item -Path $fakeKeyName -Force
    
            foreach ($n in 81 .. 89) {
                New-ItemProperty -Path $fakeKeyName -Name KitsRoot$n -Value "$script:inputDir\WDK\$n" -Force
                New-ItemProperty -Path $fakeKeyName -Name WindowsDebuggersRoot%n -value "$script:inputDir\WDK\$n\Debuggers" -Force
            }
    
            Mock Test-Path -parameterFilter {
                $Path -eq $keyName
            } -mockWith {
                Microsoft.PowerShell.Management\Test-Path -Path $fakeKeyName
            }
            
            Mock Get-Item -parameterFilter {
                $Path -eq $keyName
            } -mockWith {
                Microsoft.PowerShell.Management\Get-Item -Path $fakeKeyName
            }
    
            getWdkRoot | Should Be "$script:inputDir\WDK\89"
    
            $fk | Remove-Item
        }
    }

    Context "Copies the tools directory" {
        $toolsDir = New-Item -Path $inputDir\tools -ItemType Directory -Force
    
        It "doesn't throw an exception" {
            {New-DevboardImage @p -ToolsPath "$toolsDir"} | Should Not Throw
        }
    
        It "exports the source to the destination" {
            findCopyCommands -inputFile $toolsDir | 
                Should Be "COPYR 1 $toolsDir $script:mountDir\tools\other\tools"
        }
    }
    
    Context "Copies the WDK directory" {
        $localWdkRoot = join-path $script:inputDir "WDK"
        $debuggerMsi = join-path $localWdkRoot "Debuggers\Redist\X86 Debuggers And Tools-x86_en-us.msi"
        New-Item -Path "$localWdkRoot\tools\lots\of\sub\directories" -ItemType Directory -Force | Out-Null
        New-Item -Path $debuggerMsi -itemTYpe File -Force | Out-Null

        It "Doesn't throw an exception" {
            {New-DevboardImage @p -WdkPath $localWdkRoot} | Should Not Throw
        }
    
        It "Copies the WDK tools to the image" {
            findCopyCommands -inputFile $localWdkRoot\tools -copyType COPYR | 
                Should Be "COPYR 1 $localWdKRoot\tools $script:mountDir\tools\wdk"             
        }
    
        It "Copies the debugger MSI to the image" {
            findCopyCommands -inputFile $debuggerMsi |
                Should Be "COPY1 1 $debuggerMsi $script:mountDir\tools\debugger\$(split-path $debuggerMsi -Leaf)"
        }
    }
}

Describe "Copying Content" {
    setupTestEnvironment

    $p = getDefaultParameters

    Context "When the BSP contains a firmware image" {
        $bsp = New-BspManifest -RootDirectory $script:BspDir

        New-Item -Path "$script:BspDir\FIRMWARE.BIN" -ItemType "File" -Force | Out-Null
        $bsp.AddContent("Firmware", "$script:BspDir\FIRMWARE.BIN")

        $bsp.Save($script:bspIndex)

        It "copies the firmware to the output WIM" {
            {
                New-DevboardImage @p
            } | Should Not Throw
            
            findCopyCommands $script:BspDir\FIRMWARE.BIN | 
                Should Be "COPY1 1 $script:BspDir\FIRMWARE.BIN $script:mountDir\tools\firmware\firmware.bin"
        }
    }

}

Describe "Parameter Checking" {

    setupTestEnvironment

    $p = getDefaultParameters
    $p["BspManifest"] = "$script:inputDir\xml_file_that_doesnt_exist.xml"

    It "throws when the BspManifest does not exist" {
        {New-DevboardImage @p} | Should Throw "Cannot find BSP index $($p["BspManifest"])"
    }
}

# TODO: Find a way to test with Set-StrictMode set to 3.  Right now that seems to 
#       break Pester's internals